<!DOCTYPE html>
<html>
<head>
  <style>
    html, body {
      margin: 0;
      overflow: hidden;
      height: 100%;
    }
  </style>
  <title>WebGL training - Light</title>
</head>
<body>
  <canvas id="canvas"></canvas>
  <script type="module">
    // https://webglfundamentals.org/webgl/lessons/webgl-3d-lighting-directional.html
    // https://webglfundamentals.org/webgl/lessons/webgl-3d-lighting-point.html
    // https://webglfundamentals.org/webgl/lessons/webgl-3d-lighting-spot.html
    // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Lighting_in_WebGL
    // https://bl.ocks.org/camargo/4d3931b7bfe5b4e08570c5f8ea7e4ec1
    import {
      GUI,
      vec3,
      quat,
      mat3,
      mat4,
      toHighDPI,
      createProgram,
      createVertexArray,
      createTexture,
      Camera,
      createCube,
      createSphere,
    } from '../js/bundle.js';

    let ready = false;
    const config = {
      clearColor: [0, 0, 0],
      ambient: true,
      diffuse: true,
      specular: true,
      ambientColor: [0.3 * 255, 0.3 * 255, 0.3 * 255],
      lightDirectionX: -1,
      lightDirectionY: -2,
      lightDirectionZ: -3,
      lightDirection: [-1, -2, -3],
      lightColor: [1 * 255, 1 * 255, 1 * 255],
    };

    function setLightDirection() {
      vec3.set(
        config.lightDirection,
        config.lightDirectionX,
        config.lightDirectionY,
        config.lightDirectionZ,
      );
      vec3.normalize(config.lightDirection, config.lightDirection);
    }
    setLightDirection();
    const meshes = [
      {
        geometry: createCube(),
        material: {
          color: [255, 255, 255],
          image: '../asset/images/crate.gif',
        },
        tx: -1.5,
        ty: 0.0,
        tz: 0.0,
        sx: 1.0,
        sy: 1.0,
        sz: 1.0,
        rx: 0.0,
        ry: 0.0,
        rz: 0.0,
        translate: vec3.create(),
        scale: vec3.create(),
        quat: quat.create(),
        matrix: mat4.create(),
        normalMatrix: mat3.create(),
      },
      {
        geometry: createSphere(),
        material: {
          color: [255, 255, 255],
          image: '../asset/images/crate.gif',
        },
        tx: 1.5,
        ty: 0.0,
        tz: 0.0,
        sx: 1.0,
        sy: 1.0,
        sz: 1.0,
        rx: 0.0,
        ry: 0.0,
        rz: 0.0,
        translate: vec3.create(),
        scale: vec3.create(),
        quat: quat.create(),
        matrix: mat4.create(),
        normalMatrix: mat3.create(),
      },
    ];
    const canvas = document.getElementById('canvas');
    // 01: create WebGL context
    const gl = canvas.getContext('webgl2');
    // 02.1: create program from shader source
    const program = createProgram(gl, `#version 300 es
      precision highp float;

      layout(location = 0) in vec3 a_position;
      layout(location = 1) in vec2 a_uv;
      layout(location = 2) in vec3 a_normal;
      uniform mat4 u_modelMatrix;
      uniform mat3 u_normalMatrix;
      uniform mat4 u_viewMatrix;
      uniform mat4 u_projectionMatrix;
      out vec2 v_uv;
      out vec3 v_normal;
      out vec3 v_position;

      void main () {
        vec4 position = u_modelMatrix * vec4(a_position, 1.0);
        v_position = position.xyz;
        position = u_projectionMatrix * u_viewMatrix * position;
        gl_Position = position;
        v_uv = vec2(a_uv.x, 1.0 - a_uv.y);
        v_normal = u_normalMatrix * a_normal;
      }
    `, `#version 300 es
      precision highp float;

      uniform vec3 u_eyePosition;
      uniform vec3 u_ambientColor;
      uniform vec3 u_lightDirection;
      uniform vec3 u_lightColor;
      uniform vec3 u_color;
      uniform bool u_ambient;
      uniform bool u_diffuse;
      uniform bool u_specular;
      uniform sampler2D u_texture;
      in vec2 v_uv;
      in vec3 v_normal;
      in vec3 v_position;
      out vec4 fragColor;

      void main () {
        vec4 color = texture(u_texture, v_uv);
        vec3 baseColor = color.rgb * u_color;
        vec3 normal = normalize(v_normal);
        float diffuse = max(0.0, dot(-u_lightDirection, normal));
        float specular = 0.0;
        if (diffuse > 0.0) {
          vec3 eyeDirection = normalize(u_eyePosition - v_position);
          specular = pow(max(0.0, dot(reflect(u_lightDirection, normal), eyeDirection)), 64.0);
        }
        vec3 ambientColor = vec3(0.0);
        vec3 diffuseColor = vec3(0.0);
        vec3 specularColor = vec3(0.0);
        if (u_ambient) {
          ambientColor = u_ambientColor * baseColor;
        }
        if (u_diffuse) {
          diffuseColor = diffuse * u_lightColor * baseColor;
        }
        if (u_specular) {
          specularColor = specular * u_lightColor;
        }
        fragColor = vec4(ambientColor + diffuseColor + specularColor, 1.0);
      }
    `);
    // 02.2: get uniform locations
    const modelMatrixUniform = gl.getUniformLocation(program, 'u_modelMatrix');
    const normalMatrixUniform = gl.getUniformLocation(program, 'u_normalMatrix');
    const viewMatrixUniform = gl.getUniformLocation(program, 'u_viewMatrix');
    const projectionMatrixUniform = gl.getUniformLocation(program, 'u_projectionMatrix');
    const colorUniform = gl.getUniformLocation(program, 'u_color');
    const textureUniform = gl.getUniformLocation(program, 'u_texture');
    const eyePositionUniform = gl.getUniformLocation(program, 'u_eyePosition');
    const ambientColorUniform = gl.getUniformLocation(program, 'u_ambientColor');
    const lightDirectionUniform = gl.getUniformLocation(program, 'u_lightDirection');
    const lightColorUniform = gl.getUniformLocation(program, 'u_lightColor');
    const ambientUniform = gl.getUniformLocation(program, 'u_ambient');
    const diffuseUniform = gl.getUniformLocation(program, 'u_diffuse');
    const specularUniform = gl.getUniformLocation(program, 'u_specular');

    meshes.forEach((mesh) => {
      // 03: create vao
      mesh.vao = createVertexArray(gl, mesh.geometry);
      // calculate matrix
      vec3.set(mesh.translate, mesh.tx, mesh.ty, mesh.tz);
      vec3.set(mesh.scale, mesh.sx, mesh.sy, mesh.sz);
      quat.fromEuler(mesh.quat, mesh.rx, mesh.ry, mesh.rz);
      mat4.fromRotationTranslationScale(mesh.matrix, mesh.quat, mesh.translate, mesh.scale);
      mat3.normalFromMat4(mesh.normalMatrix, mesh.matrix);
    });

    // setup camera control
    const camera = new Camera();
    camera.attach(canvas);

    function render() {
      if (!ready) {
        return;
      }
      // 05: clean canvas before drawing
      gl.clearColor(
        config.clearColor[0] / 255,
        config.clearColor[1] / 255,
        config.clearColor[2] / 255,
        1,
      );
      // eslint-disable-next-line
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
      gl.enable(gl.DEPTH_TEST);

      // 06: use program
      gl.useProgram(program);

      // 07.1: set view matrix
      gl.uniformMatrix4fv(viewMatrixUniform, false, camera.getViewMatrix());
      // 07.2: set projection matrix
      gl.uniformMatrix4fv(projectionMatrixUniform, false, camera.getProjectionMatrix());
      // 07.3: set color uniform
      gl.uniform3f(
        ambientColorUniform,
        config.ambientColor[0] / 255,
        config.ambientColor[1] / 255,
        config.ambientColor[2] / 255,
      );
      gl.uniform3fv(lightDirectionUniform, config.lightDirection);
      gl.uniform3f(
        lightColorUniform,
        config.lightColor[0] / 255,
        config.lightColor[1] / 255,
        config.lightColor[2] / 255,
      );
      gl.uniform3fv(eyePositionUniform, camera.eye);
      gl.uniform1i(ambientUniform, config.ambient);
      gl.uniform1i(diffuseUniform, config.diffuse);
      gl.uniform1i(specularUniform, config.specular);

      meshes.forEach((mesh) => {
        // 08.1: set color uniform
        gl.uniform3f(
          colorUniform,
          mesh.material.color[0] / 255,
          mesh.material.color[1] / 255,
          mesh.material.color[2] / 255,
        );
        // 08.2: set texture uniform
        gl.uniform1i(textureUniform, 0);
        // 08.3: set model matrix uniform
        gl.uniformMatrix4fv(modelMatrixUniform, false, mesh.matrix);
        // 08.4: set normal matrix uniform
        gl.uniformMatrix3fv(normalMatrixUniform, false, mesh.normalMatrix);

        // 09: bind texture
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, mesh.material.texture);

        // 10: bind vao
        gl.bindVertexArray(mesh.vao);

        // 11: draw triangles
        if (mesh.geometry.indices) {
          gl.drawElements(gl.TRIANGLES, mesh.geometry.count, gl.UNSIGNED_SHORT, 0);
        } else {
          gl.drawArrays(gl.TRIANGLES, 0, mesh.geometry.count);
        }
      });

      gl.useProgram(null);
    }

    // handle resize
    function resize() {
      const { clientWidth, clientHeight } = document.documentElement;
      canvas.width = clientWidth;
      canvas.height = clientHeight;
      // handle high DPI screen
      toHighDPI(canvas);
      // set viewport
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      render();
    }

    window.onresize = resize;
    resize();

    // 04: create texture
    (async () => {
      const textures = await Promise.all(meshes.map(mesh => createTexture(gl, mesh.material.image)));
      textures.forEach((texture, i) => {
        meshes[i].material.texture = texture;
      });
      ready = true;
      render();
    })();

    camera.onZoom = (scale) => {
      render();
    };
    camera.onPan = (x, y) => {
      render();
    };
    camera.onRotate = (x, y) => {
      render();
    };

    // config
    const gui = new GUI();
    gui.addColor(config, 'clearColor').onChange(() => {
      render();
    });
    const lightFolder = gui.addFolder('Light');
    lightFolder.add(config, 'ambient').onChange(() => {
      render();
    });
    lightFolder.add(config, 'diffuse').onChange(() => {
      render();
    });
    lightFolder.add(config, 'specular').onChange(() => {
      render();
    });
    lightFolder.addColor(config, 'ambientColor').onChange(() => {
      render();
    });
    lightFolder.addColor(config, 'lightColor').onChange(() => {
      render();
    });
    lightFolder.add(config, 'lightDirectionX', -10, 10, 0.1).onChange(() => {
      setLightDirection();
      render();
    });
    lightFolder.add(config, 'lightDirectionY', -10, 10, 0.1).onChange(() => {
      setLightDirection();
      render();
    });
    lightFolder.add(config, 'lightDirectionZ', -10, 10, 0.1).onChange(() => {
      setLightDirection();
      render();
    });
  </script>
</body>
</html>